local Class = require('lua.lang.Class')
local LuaString = require('lua.vm.LuaString')
local LuaValue = require('lua.vm.LuaValue')
local Double = require('lua.lang.Double')
local LoadState = require('lua.vm.LoadState')
local DataOutputStream = require('lua.io.DataOutputStream')

--- @class DumpState : Class
--- @field IS_LITTLE_ENDIAN boolean
--- @field NUMBER_FORMAT number
--- @field SIZEOF_LUA_NUMBER number
--- @field writer DataOutputStream
--- @field strip boolean
--- @field status number
local DumpState = Class {}

DumpState.ALLOW_INTEGER_CASTING = false
DumpState.NUMBER_FORMAT_FLOATS_OR_DOUBLES = 0
DumpState.NUMBER_FORMAT_INTS_ONLY = 1
DumpState.NUMBER_FORMAT_NUM_PATCH_INT32 = 4
DumpState.NUMBER_FORMAT_DEFAULT = DumpState.NUMBER_FORMAT_FLOATS_OR_DOUBLES

DumpState.SIZEOF_INT = 4
DumpState.SIZEOF_SIZET = 4
DumpState.SIZEOF_INSTRUCTION = 4

--- @param w OutputStream
--- @param strip boolean
--- @return DumpState
function DumpState:new(w, strip)
	--- @type DumpState
	local o = {}
	o.IS_LITTLE_ENDIAN = false
	o.NUMBER_FORMAT = DumpState.NUMBER_FORMAT_DEFAULT
	o.SIZEOF_LUA_NUMBER = 8
	o.writer = DataOutputStream:new(w)
	o.strip = strip
	o.status = 0
	return setmetatable(o, DumpState)
end

--- @param b number
--- @return void
function DumpState:dumpByte(b)
	self.writer:write(b)
end

--- @param c string
--- @return void
function DumpState:dumpChar(c)
	self:dumpByte(c:byte())
end

--- @param x number
--- @return void
function DumpState:dumpInt(x)
	if self.IS_LITTLE_ENDIAN then
		self.writer:writeByte(x & 0xff)
		self.writer:writeByte((x >> 8) & 0xff)
		self.writer:writeByte((x >> 16) & 0xff)
		self.writer:writeByte((x >> 24) & 0xff)
	else
		self.writer:writeInt(x)
	end
end

--- @param s LuaString
--- @return void
function DumpState:dumpString(s)
	s = s or LuaString:valueOf('')
	local bytes = s:toLString():toCharArray()
	local len = #bytes
	self:dumpInt(len + 1)
	for i = 1, len do
		self.writer:write(bytes[i])
	end
	self.writer:write(0)
end

--- @param d Double
--- @return void
function DumpState:dumpDouble(d)
	local l = Double:doubleToLongBits(d)
	if self.IS_LITTLE_ENDIAN then
		self:dumpInt(l)
		self:dumpInt(l >> 32)
	else
		self.writer:writeLong(l)
	end
end

--- @param p Prototype
--- @return void
function DumpState:dumpCode(p)
	local code = p.code
	local n = #code
	self:dumpInt(n)
	for i = 1, n do
		self:dumpInt(code[i])
	end
end

--- @param f Prototype
--- @return void
function DumpState:dumpConstants(f)
	local k = f.k
	local n = #k
	local writer = self.writer
	self:dumpInt(n)
	for i = 1, n do
		local o = k[i]
		local type = o:type()
		if type == LuaValue.TYPE_NIL then
			writer:write(LuaValue.TYPE_NIL)
		elseif type == LuaValue.TYPE_BOOLEAN then
			writer:write(LuaValue.TYPE_BOOLEAN)
			self:dumpByte(o:toBoolean() and 1 or 0)
		elseif type == LuaValue.TYPE_NUMBER then
			local format = self.NUMBER_FORMAT
			if format == self.NUMBER_FORMAT_FLOATS_OR_DOUBLES then
				writer:write(LuaValue.TYPE_NUMBER)
				self:dumpDouble(o:toNumber())
			elseif format == self.NUMBER_FORMAT_INTS_ONLY then
				if not self.ALLOW_INTEGER_CASTING and not o:isInt() then
					error('not an integer: ' .. o:toLString())
				end
				writer:write(LuaValue.TYPE_NUMBER)
				self:dumpInt(o:toInt())
			elseif format == self.NUMBER_FORMAT_NUM_PATCH_INT32 then
				if o:isInt() then
					writer:write(LuaValue.TYPE_INT)
					self:dumpInt(o:toInt())
				else
					writer:write(LuaValue.TYPE_NUMBER)
					self:dumpDouble(o:toNumber())
				end
			else
				error('number format not supported: ' .. tostring(format))
			end
		elseif type == LuaValue.TYPE_STRING then
			writer:write(LuaValue.TYPE_STRING)
			self:dumpString(o)
		else
			error('bad value type: ' .. tostring(type))
		end
	end
	n = #f.p
	self:dumpInt(n)
	for i = 1, n do
		self:dumpFunction(f.p[i])
	end
end

--- @param f Prototype
--- @return void
function DumpState:dumpUpvalues(f)
	local upvalues = f.upvalues
	local n = #upvalues
	self:dumpInt(n)
	for i = 1, n do
		local uv = upvalues[i]
		self:dumpByte(uv.isInStack and 1 or 0)
		self:dumpByte(uv.index)
	end
end

--- @param f Prototype
--- @return void
function DumpState:dumpDebug(f)
	local strip = self.strip
	if strip then
		self:dumpInt(0)
	else
		self:dumpString(f.source)
	end
	local n = strip and 0 or #f.lineInfo
	self:dumpInt(n)
	for i = 1, n do
		self:dumpInt(f.lineInfo[i])
	end
	n = strip and 0 or #f.locVars
	self:dumpInt(n)
	for i = 1, n do
		local lv = f.locVars[i]
		self:dumpString(lv.varName)
		self:dumpInt(lv.startPc)
		self:dumpInt(lv.endPc)
	end
	n = strip and 0 or #f.upvalues
	self:dumpInt(n)
	for i = 1, n do
		self:dumpString(f.upvalues[i].variableName)
	end
end

--- @param f Prototype
--- @return void
function DumpState:dumpFunction(f)
	self:dumpInt(f.lineDefined)
	self:dumpInt(f.lastLineDefined)
	self:dumpByte(f.numParams)
	self:dumpByte(f.isVararg)
	self:dumpByte(f.maxStackSize)
	self:dumpCode(f)
	self:dumpConstants(f)
	self:dumpUpvalues(f)
	self:dumpDebug(f)
end

--- @return void
function DumpState:dumpHeader()
	local writer = self.writer
	local sign = LoadState.LUA_SIGNATURE
	for i = 1, #sign do
		writer:write(sign[i])
	end
	writer:write(LoadState.LUAC_VERSION)
	writer:write(LoadState.LUAC_FORMAT)
	writer:write(self.IS_LITTLE_ENDIAN and 1 or 0)
	writer:write(self.SIZEOF_INT)
	writer:write(self.SIZEOF_SIZET)
	writer:write(self.SIZEOF_INSTRUCTION)
	writer:write(self.SIZEOF_LUA_NUMBER)
	writer:write(self.NUMBER_FORMAT)
	local tail = LoadState.LUAC_TAIL
	for i = 1, #tail do
		writer:write(tail[i])
	end
end

--- @param f Prototype
--- @param w OutputStream
--- @param stripDebug boolean
--- @param numberFormat number
--- @param littleEndian boolean
--- @return void
function DumpState:dump(f, w, stripDebug, numberFormat, littleEndian)
	numberFormat = numberFormat or self.NUMBER_FORMAT_DEFAULT
	if numberFormat ~= self.NUMBER_FORMAT_FLOATS_OR_DOUBLES and
			numberFormat ~= self.NUMBER_FORMAT_INTS_ONLY and
			numberFormat ~= self.NUMBER_FORMAT_NUM_PATCH_INT32 then
		error('number format not supported: ' .. tostring(numberFormat))
	end
	littleEndian = littleEndian or true
	local state = DumpState:new(w, stripDebug)
	state.IS_LITTLE_ENDIAN = littleEndian
	state.NUMBER_FORMAT = numberFormat
	state.SIZEOF_LUA_NUMBER = numberFormat == self.NUMBER_FORMAT_INTS_ONLY and 4 or 8
	state:dumpHeader()
	state:dumpFunction(f)
	return state.status
end

return DumpState